// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: wsfuyibing <682805@qq.com>
// Date: 2024-07-18

package src

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

// Command
// is a component used to schedule in container.
type Command struct {
	defaulter         bool
	keys              map[string]string
	name, description string
	options           map[string]*Option
	provider          Provider
}

// NewCommand
// creates a new command instance.
func NewCommand(name string) *Command {
	return &Command{
		name:    name,
		keys:    make(map[string]string),
		options: make(map[string]*Option),
	}
}

// GenerateModule
// generate application module name and working path relatives with
// working directory.
func (o *Command) GenerateModule(path string) (name, folder string) {
	var (
		body        []byte
		err         error
		folders     = make([]string, 0)
		info        os.FileInfo
		src         string
		regex       = regexp.MustCompile(`^([a-zA-Z]+):`)
		regexFolder = regexp.MustCompile(`/([^/]+)$`)
		regexModule = regexp.MustCompile(`module\s+([^\n]+)`)
	)

	// Scan until root.
	for {
		// Return error
		// if calculate absolute path failed.
		if path, err = filepath.Abs(path); err != nil {
			break
		}

		// Break loop
		// until root path.
		if path == "/" || regex.MatchString(path) {
			break
		}

		// Check file
		// for go.mod of an application.
		src = fmt.Sprintf(`%s/go.mod`, path)

		if info, err = os.Stat(src); err != nil {
			// Goto
			// parent folder if module file not found.
			if errors.Is(err, os.ErrNotExist) {
				// Collect
				// folder name.
				if m := regexFolder.FindStringSubmatch(path); len(m) > 0 {
					folders = append(folders, m[1])
				}

				// Reset and goto parent.
				err = nil
				path = fmt.Sprintf(`%s/../`, path)
				continue
			}

			// Break
			// for stat failed.
			break
		}

		// Break
		// for target is a directory.
		if info.IsDir() {
			break
		}

		// Match content.
		if body, err = os.ReadFile(src); err == nil {
			if m := regexModule.FindStringSubmatch(string(body)); len(m) > 0 {
				name = strings.TrimSpace(m[1])
			}
		}
		break
	}

	// Build folders.
	if name != "" {
		if n := len(folders); n > 0 {
			ks := make([]string, 0)
			for i := n - 1; i >= 0; i-- {
				ks = append(ks, folders[i])
			}

			folder = strings.Join(ks, "/")
		}
	}
	return
}

// Add
// adds an options list into command.
func (o *Command) Add(options ...*Option) {
	// Iterate options in parameters.
	for _, opt := range options {
		// Ignore nil option.
		if opt == nil {
			continue
		}

		// Ignore duplicated option.
		if _, ok := o.options[opt.name]; ok {
			continue
		}

		// Set mapping with full name.
		o.options[opt.name] = opt

		// Set keys mapping with full name.
		o.keys[opt.name] = opt.name

		// Set keys mapping with short name.
		if opt.shortName != "" {
			o.keys[opt.shortName] = opt.name
		}
	}
}

// GetDescription
// returns a command description.
func (o *Command) GetDescription() string {
	return o.description
}

// GetName
// returns a command name.
//
// Code:
//   println(cmd.GetName())
//
// Output:
//   example
func (o *Command) GetName() string {
	return o.name
}

// GetOption
// returns an option in command if exists.
//
// Code:
//   opt, has := cmd.GetOption("key")
//
// Output:
//   &Option{}, true
func (o *Command) GetOption(key string) (opt *Option, has bool) {
	if name, ok := o.keys[key]; ok {
		opt, has = o.options[name]
	}
	return
}

// GetOptions
// returns an option mapping.
//
// Output:
//   {
//       "key": &Option{...},
//   }
func (o *Command) GetOptions() map[string]*Option {
	return o.options
}

// SetDescription
// set description for command.
func (o *Command) SetDescription(description string) *Command {
	o.description = strings.TrimSpace(description)
	return o
}

// SetProvider
// set command provider. Method's in provider will be called when scheduler fired.
func (o *Command) SetProvider(provider Provider) *Command {
	o.provider = provider
	return o
}

// ToScript
// convert runtime command as runnable script.
//
//   gen:model --key="value" --null
func (o *Command) ToScript() (script string) {
	var (
		keys   = make([]string, 0)
		mapper = make(map[string]*Option)
	)

	// Iterate
	// options in command and specified.
	for _, opt := range o.options {
		// Ignore
		// nil or not specified option.
		if opt == nil || !opt.GetSpecified() {
			continue
		}

		// Append
		// to list.
		keys = append(keys, opt.name)
		mapper[opt.name] = opt
	}

	// Build
	// with sorted keys.
	script = o.name
	for _, key := range keys {
		if opt, ok := mapper[key]; ok {
			if opt.kind == KindNull {
				script += fmt.Sprintf(` --%s`, opt.name)
			} else {
				script += fmt.Sprintf(` --%s="%s"`, opt.name, opt.ToString())
			}
		}
	}
	return
}

// +---------------------------------------------------------------------------+
// | Access methods                                                            |
// +---------------------------------------------------------------------------+

// FormatDescription
// returns a string list for description if set and not empty.
func (o *Command) formatDescription() []string {
	if o.description == "" {
		return []string{}
	}
	return strings.Split(o.description, "\n")
}

// Run
// runs the command by schedule provider methods.
func (o *Command) run(ctx context.Context, container *Container) {
	var err error

	// Before handler.
	if v, ok := o.provider.(ProviderBefore); ok {
		if err = v.Before(ctx, container, o); err != nil {
			container.GetOutput().Error(`%v`, err)
			return
		}
	}

	// Runner handler.
	if err = o.provider.Run(ctx, container, o); err != nil {
		container.GetOutput().Error(`%v`, err)
	}

	// After handler.
	if v, ok := o.provider.(ProviderAfter); ok {
		v.After(ctx, container, o, err)
	}
}

// Verify
// verifies the command access.
func (o *Command) verify(container *Container) (success bool) {
	// Iterate
	// configuration in config file then assign it.
	if c := Config.GetCommand(o.name); c != nil {
		for k, v := range c.Options {
			if opt, has := o.GetOption(k); has {
				opt.assign(v)
				continue
			}
			container.GetOutput().Error(`unsupported config option: %s`, k)
			return false
		}
	}

	// Iterate
	// options in command-line arguments then assign it.
	for k, v := range container.argument.GetOptions() {
		// Defined in command-line arguments.
		if opt, has := o.GetOption(k); has {
			opt.assign(v)
			continue
		}

		// Not defined in console.
		if len(k) == 1 {
			container.GetOutput().Error(`unsupported option: -%s`, k)
		} else {
			container.GetOutput().Error(`unsupported option: --%s`, k)
		}
		return false
	}

	// Validate
	// command options.
	for _, opt := range o.options {
		if err := opt.validate(); err != nil {
			container.GetOutput().Error(`%v`, err)
			return false
		}
	}
	return true
}
